/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/*
 * This code contains code from SpeedTracer's Canvas implementation, as well as code 
 * ripped from Button and Image widgets. Think Frankenstein. But worse.
 * 
 * bjm
 */

package com.domesticmouse.waveoteditor.client.canvas;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasAllMouseHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

public class Canvas extends Widget implements HasClickHandlers, HasAllMouseHandlers {

	public Canvas() {
		this(Document.get().createElement(CanvasElement.TAG));
	}

	public Canvas(Element element) {
		this.setElement(element);
		CanvasElement.as(element);
		setGfxContext(element);
	}

	/**
	 * Creates a Canvas widget that wraps an existing &lt;canvas&gt; element.
	 * 
	 * This element must already be attached to the document. If the element is
	 * removed from the document, you must call
	 * {@link RootPanel#detachNow(Widget)}.
	 * 
	 * @param element
	 *            the element to be wrapped
	 */
	public static Canvas wrap(Element element) {
		// Assert that the element is attached.
		assert Document.get().getBody().isOrHasChild(element);

		Canvas canvas = new Canvas(element);

		// Mark it attached and remember it for cleanup.
		canvas.onAttach();
		RootPanel.detachOnWindowClose(canvas);

		// I suspect this is wrong, but firefox isn't playing ball without it.
		// bjm
		canvas.sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);

		return canvas;
	}

	@Override
	public HandlerRegistration addClickHandler(ClickHandler handler) {
		return addHandler(handler, ClickEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
		return addDomHandler(handler, MouseDownEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
		return addDomHandler(handler, MouseMoveEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
		return addDomHandler(handler, MouseOutEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
		return addDomHandler(handler, MouseOverEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
		return addDomHandler(handler, MouseUpEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
		return addDomHandler(handler, MouseWheelEvent.getType());
	}

	// This is used from JSNI functions. Ignore warning.
	@SuppressWarnings("unused")
	private JavaScriptObject canvasContext = null;

	private native void setGfxContext(Element e) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::setCanvasContext(Lcom/google/gwt/core/client/JavaScriptObject;)(e.getContext('2d')));
	}-*/;

	// This is called from above JSNI function. Ignore warning.
	@SuppressWarnings("unused")
	private void setCanvasContext(JavaScriptObject ctx) {
		this.canvasContext = ctx;
	}

	/**
	 * Draws an arc. If the context has a non-empty path, then the method must
	 * add a straight line from the last point in the path to the start point of
	 * the arc.
	 * 
	 * @param x
	 *            center X coordinate
	 * @param y
	 *            center Y coordinate
	 * @param radius
	 *            radius of drawn arc
	 * @param startAngle
	 *            angle measured from positive X axis to start of arc CW
	 * @param endAngle
	 *            angle measured from positive X axis to end of arc CW
	 * @param antiClockwise
	 *            direction that the arc line is drawn
	 */
	public native void arc(double x, double y, double radius, double startAngle, double endAngle, boolean antiClockwise) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).arc(x,y,radius,startAngle,endAngle,antiClockwise);
	}-*/;

	/**
	 * Erases the current path and prepares it for a new path.
	 */
	public native void beginPath() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).beginPath();
	}-*/;

	/**
	 * Clears the entire canvas.
	 */
	public void clear() {
		// Canvas width and height should be stored locally for performance
		// reasons. bjm
		this.clearRect(0, 0, getElement().getPropertyInt("width"), getElement().getPropertyInt("height"));
	}

	public native void clearRect(double startX, double startY, double width, double height) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).clearRect(startX,startY,width,height);
	}-*/;

	/**
	 * Closes the current path. "Closing" simply means that a line is drawn from
	 * the last element in the path back to the first.
	 */
	public native void closePath() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).closePath();
	}-*/;

	/**
	 * 
	 * Does nothing if the context's path is empty. Otherwise, it connects the
	 * last point in the path to the given point <b>(x, y)</b> using a cubic
	 * Bezier curve with control points <b>(cp1x, cp1y)</b> and <b>(cp2x,
	 * cp2y)</b>. Then, it must add the point <b>(x, y)</b> to the path.
	 * 
	 * This function corresponds to the
	 * <code>bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)</code> method in canvas
	 * element Javascript API.
	 * 
	 * @param cp1x
	 *            x coord of first Control Point
	 * @param cp1y
	 *            y coord of first Control Point
	 * @param cp2x
	 *            x coord of second Control Point
	 * @param cp2y
	 *            y coord of second Control Point
	 * @param x
	 *            x coord of point
	 * @param y
	 *            x coord of point
	 */
	public native void cubicCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y);
	}-*/;

	/**
	 * Fills the current path according to the current fillstyle.
	 */
	public native void fill() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).fill();
	}-*/;

	/**
	 * Fills a rectangle of the specified dimensions, at the specified start
	 * coords, according to the current fillstyle.
	 * 
	 * @param startX
	 *            x coord of the top left corner in the destination space
	 * @param startY
	 *            y coord of the top left corner in the destination space
	 * @param width
	 *            destination width of image
	 * @param height
	 *            destination height of image
	 */
	public native void fillRect(double startX, double startY, double width, double height) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).fillRect(startX,startY,width,height);
	}-*/;

	/**
	 * Adds a line from the last point in the current path to the point defined
	 * by x and y.
	 * 
	 * @param x
	 *            x coord of point
	 * @param y
	 *            y coord of point
	 */
	public native void lineTo(double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).lineTo(x,y);
	}-*/;

	/**
	 * Makes the last point in the current path be <b>(x,y)</b>.
	 * 
	 * @param x
	 *            x coord of point
	 * @param y
	 *            y coord of point
	 */
	public native void moveTo(double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).moveTo(x,y);
	}-*/;

	/**
	 * Does nothing if the context has an empty path. Otherwise it connects the
	 * last point in the path to the given point <b>(x, y)</b> using a quadratic
	 * Bezier curve with control point <b>(cpx, cpy)</b>, and then adds the
	 * given point <b>(x, y)</b> to the path.
	 * 
	 * @param cpx
	 *            x coord of the control point
	 * @param cpy
	 *            y coord of the control point
	 * @param x
	 *            x coord of the point
	 * @param y
	 *            y coord of the point
	 */

	public native void quadraticCurveTo(double cpx, double cpy, double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).quadraticCurveTo(cpx,cpy,x,y);
	}-*/;

	/**
	 * Adds a rectangle to the current path, and closes the path.
	 * 
	 * @param x
	 *            x coord of the top left corner of the rectangle
	 * @param y
	 *            y coord of the top left corner of the rectangle
	 * @param width
	 *            the width of the rectangle
	 * @param height
	 *            the height of the rectangle
	 */
	public native void rect(double x, double y, double width, double height) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).rect(x,y,width,height);
	}-*/;

	/**
	 * Restores the last saved context from the context stack.
	 */
	public native void restoreContext() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).restore();
	}-*/;

	/**
	 * Adds a rotation of the specified angle to the current transform.
	 * 
	 * @param angle
	 *            the angle to rotate by, <b>in radians</b>
	 */
	public native void rotate(double angle) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).rotate(angle);
	}-*/;

	/**
	 * Saves the current context to the context stack.
	 */
	public native void saveContext() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).save();
	}-*/;

	/**
	 * Adds a scale transformation to the current transformation matrix.
	 * 
	 * @param x
	 *            ratio that we must scale in the X direction
	 * @param y
	 *            ratio that we must scale in the Y direction
	 */
	public native void scale(double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).scale(x,y);
	}-*/;

	/**
	 * Set the current Fill Style to the specified color.
	 * 
	 * @param color
	 *            {@link Color}
	 */
	public void setFillStyle(Color color) {
		setFillStyle(color.toString());
	}

	/**
	 * Set the global transparency to the specified alpha.
	 * 
	 * @param alpha
	 *            alpha value
	 */
	public native void setGlobalAlpha(double alpha) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).globalAlpha = alpha;
	}-*/;

	/**
	 * Sets the current context's linewidth. Line width is the thickness of a
	 * stroked line.
	 * 
	 * @param width
	 *            the width of the stroked line.
	 */
	public native void setLineWidth(double width) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).lineWidth = width;
	}-*/;

	/**
	 * Set the current Stroke Style to the specified color.
	 * 
	 * @param color
	 *            {@link Color}
	 */
	public void setStrokeStyle(Color color) {
		setStrokeStyle(color.toString());
	}

	/**
	 * Strokes the current path according to the current stroke style.
	 */
	public native void stroke() /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).stroke();
	}-*/;

	/**
	 * Strokes a rectangle defined by the supplied arguments.
	 * 
	 * @param startX
	 *            x coord of the top left corner
	 * @param startY
	 *            y coord of the top left corner
	 * @param width
	 *            width of the rectangle
	 * @param height
	 *            height of the rectangle
	 */
	public native void strokeRect(double startX, double startY, double width, double height) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).strokeRect(startX,startY,width,height);
	}-*/;

	/**
	 * <code>The transform(m11, m12, m21, m22, dx, dy)</code> method must
	 * multiply the current transformation matrix with the input matrix. Input
	 * described by:
	 * 
	 * <pre>
	 * m11   m21   dx
	 * m12   m22   dy
	 * 0      0     1
	 *</pre>
	 * 
	 * @param m11
	 *            top left cell of 2x2 rotation matrix
	 * @param m12
	 *            top right cell of 2x2 rotation matrix
	 * @param m21
	 *            bottom left cell of 2x2 rotation matrix
	 * @param m22
	 *            bottom right cell of 2x2 rotation matrix
	 * @param dx
	 *            Translation in X direction
	 * @param dy
	 *            Translation in Y direction
	 */
	public native void transform(double m11, double m12, double m21, double m22, double dx, double dy) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).transform(m11,m12,m21,m22,dx,dy);
	}-*/;

	/**
	 * Applies a translation (linear shift) by x in the horizontal and by y in
	 * the vertical.
	 * 
	 * @param x
	 *            amount to shift in the x direction
	 * @param y
	 *            amount to shift in the y direction
	 */
	public native void translate(double x, double y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).translate(x,y);
	}-*/;

	private native void setFillStyle(String colorStr) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).fillStyle = colorStr;
	}-*/;

	private native void setStrokeStyle(String colorStr) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).strokeStyle = colorStr;
	}-*/;

	public native void setFont(String font) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).font = font;
	}-*/;

	public native void strokeText(String text, int x, int y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).strokeText(text, x, y);
	}-*/;

	public native void fillText(String text, int x, int y) /*-{
		(this.@com.domesticmouse.waveoteditor.client.canvas.Canvas::canvasContext).fillText(text, x, y);
	}-*/;

}
